import hashlib,json,logging,re,subprocess,os,sys,tempfile,time,tkinter as tk
from tkinter import messagebox,ttk,filedialog,scrolledtext
import threading,webbrowser
from packaging import version
import requests,urllib
CURRENT_VERSION="2.5.3"
def setup_logging():
 l=os.path.join(os.path.dirname(sys.executable),"logs")
 os.makedirs(l,exist_ok=True)
 f=os.path.join(l,f"update_{time.strftime('%Y%m%d')}.log")
 logging.basicConfig(filename=f,level=logging.DEBUG,format='%(asctime)s - %(levelname)s - %(message)s',filemode='a')
 return logging.getLogger()
logger=setup_logging()
def check_for_updates():
 try:
  logger.info("开始检查更新...")
  r=requests.get("https://gitee.com/mcpsde/mcz-spectral-tool/raw/master/update.json",timeout=10)
  r.raise_for_status()
  u=r.json()
  logger.info(f"获取到更新信息: {json.dumps(u, ensure_ascii=False)}")
  if version.parse(u["version"])>version.parse(CURRENT_VERSION):
   logger.info(f"发现新版本: {u['version']}")
   return{"available":True,"version":u["version"],"download_url":u["download_url"],"release_notes":"\n".join(u["changelog"]),"file_size":u.get("file_size",0),"sha256":u.get("sha256","")}
 except Exception as e:logger.error(f"检查更新失败: {str(e)}",exc_info=True)
 logger.info("未发现新版本")
 return{"available":False}
def verify_file_integrity(f,s,h):
 logger.info(f"开始验证文件: {f}")
 try:
  sha256=hashlib.sha256()
  with open(f,'rb')as file:
   while True:
    d=file.read(65536)
    if not d:break
    sha256.update(d)
  a=sha256.hexdigest()
  if a!=h:logger.error(f"哈希值不匹配: {a} vs {h}");return False,f"哈希值不匹配: {a} vs {h}"
  logger.info("文件验证成功")
  return True,""
 except Exception as e:logger.error(f"文件验证失败: {str(e)}",exc_info=True);return False,f"文件验证失败: {str(e)}"
def create_restart_script(n,a):
 try:
  logger.info("创建重启脚本...")
  b=os.path.join(a,"restart.bat")
  c=sys.executable
  e=os.path.basename(c)
  with open(b,'w',encoding='gbk')as f:
   f.write("@echo off\n")
   f.write("chcp 65001 >nul\n")
   f.write("echo 正在更新应用程序...\n")
   f.write("timeout /t 3 /nobreak >nul\n")
   f.write(f'echo 正在替换文件: "{e}"\n')
   f.write(f'del /F /Q "{c}"\n')
   f.write(f'copy /Y "{n}" "{a}\\" >nul\n')
   f.write(f'start "" "{a}\\{os.path.basename(n)}"\n')
   f.write("echo 更新完成，正在启动新版本...\n")
   f.write("timeout /t 2 /nobreak >nul\n")
   f.write("del \"%~f0\"\n")
  logger.info(f"创建重启脚本成功: {b}")
  return b
 except Exception as e:logger.error(f"创建重启脚本失败: {str(e)}",exc_info=True);raise
def download_with_progress(u,f,p=None):
 try:
  logger.info(f"开始下载: {u}")
  r=requests.get(u,stream=True,timeout=30)
  r.raise_for_status()
  t=int(r.headers.get('content-length',0))
  b=1024*1024
  d=0
  with open(f,'wb')as file:
   for data in r.iter_content(b):
    file.write(data)
    d+=len(data)
    if t>0 and p:percent=min(100,int(d*100/t));p(percent)
  logger.info(f"下载完成: {f}")
  return True
 except Exception as e:logger.error(f"下载失败: {str(e)}");return False
def start_update(u,d=None):
 try:
  logger.info("开始更新过程...")
  a=os.path.dirname(sys.executable)
  t=tempfile.mkdtemp()
  f=u["download_url"].split('/')[-1]
  n=os.path.join(t,f)
  if d and hasattr(d,'status_label'):d.status_label.config(text="正在下载更新...");d.update()
  def update_progress(percent):
   if d and hasattr(d,'progress')and hasattr(d,'percent_label'):d.progress['value']=percent;d.percent_label.config(text=f"{percent}%");d.update()
  s=download_with_progress(u["download_url"],n,update_progress)
  if not s:raise Exception("下载失败")
  if d and hasattr(d,'status_label'):d.status_label.config(text="正在验证文件...");d.update()
  if u.get("file_size")and u.get("sha256"):
   v,r=verify_file_integrity(n,u["file_size"],u["sha256"])
   if not v:raise Exception(f"文件验证失败: {r}")
  if d and hasattr(d,'status_label'):d.status_label.config(text="准备重启...");d.update()
  b=create_restart_script(n,a)
  restart_application(b)
 except Exception as e:
  logger.error(f"更新失败: {str(e)}",exc_info=True)
  m=f"更新过程中出错: {str(e)}"
  if d:
   if hasattr(d,'status_label'):d.status_label.config(text=m)
   d.update();time.sleep(2);d.destroy()
  if messagebox.askyesno("更新失败",f"{m}\n\n是否手动下载新版本？"):webbrowser.open(u["download_url"])
def restart_application(b):
 try:messagebox.showerror("更新完毕!请重新启动");sys.exit(0)
 except Exception as e:logger.error(f"重启失败: {str(e)}",exc_info=True);messagebox.showerror("重启失败",f"无法重启应用程序: {str(e)}\n请手动运行: {b}")
def check_updates_on_start():root.after(2000,lambda:threading.Thread(target=check_and_show_updates,daemon=True).start())
def check_and_show_updates():
 try:
  u=check_for_updates()
  if u["available"]:root.after(0,lambda:show_update_dialog(u))
 except Exception as e:logger.error(f"更新检查失败: {str(e)}",exc_info=True)
def show_update_dialog(u):
 try:
  logger.info("显示更新对话框...")
  d=tk.Toplevel(root)
  d.title("发现新版本")
  d.geometry("809x500")
  d.transient(root)
  d.grab_set()
  try:d.iconbitmap(resource_path("asd.ico"))
  except:pass
  f=ttk.Frame(d,padding=10)
  f.pack(fill=tk.BOTH,expand=True)
  t=ttk.Label(f,text=f"发现新版本 {u['version']} (当前版本: {CURRENT_VERSION})",font=("Arial",12,"bold"))
  t.pack(pady=10)
  n=ttk.LabelFrame(f,text="更新说明")
  n.pack(fill=tk.BOTH,expand=True,pady=5)
  s=scrolledtext.ScrolledText(n,wrap=tk.WORD,height=8)
  s.pack(fill=tk.BOTH,expand=True,padx=5,pady=5)
  s.insert(tk.END,u.get("release_notes","无更新说明"))
  s.config(state=tk.DISABLED)
  b=ttk.Frame(f)
  b.pack(fill=tk.X,pady=10)
  dl=ttk.Button(b,text="立即更新",command=lambda:start_update(u,d),width=15)
  dl.pack(side=tk.LEFT,padx=5)
  la=ttk.Button(b,text="稍后提醒",command=d.destroy,width=15)
  la.pack(side=tk.RIGHT,padx=5)
  m=ttk.Button(b,text="手动下载",command=lambda:webbrowser.open(u["download_url"]),width=15)
  m.pack(side=tk.RIGHT,padx=5)
  logger.info("更新对话框显示成功")
  return d
 except Exception as e:logger.error(f"显示更新对话框失败: {str(e)}",exc_info=True);messagebox.showerror("错误",f"无法显示更新对话框: {str(e)}")
def resource_path(r):
 try:b=sys._MEIPASS
 except Exception:b=os.path.abspath(".")
 return os.path.join(b,r)
def create_silence(d,o,s=44100):
 if d<=0:return
 t=d/1000.0
 subprocess.run(['ffmpeg','-y','-f','lavfi','-i',f'anullsrc=r={s}:cl=stereo','-t',str(t),'-c:a','libvorbis',o],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
def process_audio(i,o,d,ou,s=44100):
 t=tempfile.mkdtemp()
 try:
  if o!=0:
   s=abs(o)/1000.0
   if o>0:
    si=os.path.join(t,"silence.ogg")
    create_silence(o,si,s)
    t1=os.path.join(t,"offset.ogg")
    subprocess.run(['ffmpeg','-y','-i',si,'-i',i,'-filter_complex','[0:a][1:a]concat=n=2:v=0:a=1','-c:a','libvorbis',t1],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
   else:
    t1=os.path.join(t,"offset.ogg")
    subprocess.run(['ffmpeg','-y','-ss',str(s),'-i',i,'-c:a','copy',t1],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
  else:t1=i
  du=d/1000.0
  subprocess.run(['ffmpeg','-y','-i',t1,'-t',str(du),'-c:a','libvorbis','-avoid_negative_ts','make_zero',ou],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
 finally:
  if os.path.exists(t):
   for f in os.listdir(t):os.remove(os.path.join(t,f))
   os.rmdir(t)
def outputmixogg(a,p,r,o="mix.ogg"):
 t=tempfile.mkdtemp()
 try:
  pr=[]
  for i,(au,pa)in enumerate(zip(a,p)):
   b=pa['bpm']
   off=pa['offset']
   be=pa['beats']
   du=(be+2/b)*60*1000
   ou=os.path.join(t,f"processed_{i}.ogg")
   process_audio(au,off,du,ou)
   pr.append(ou)
  re=None
  if r>0:re=os.path.join(t,"rest.ogg");create_silence(r,re)
  c=[]
  for i,f in enumerate(pr):
   c.append(f)
   if re and i<len(pr)-1:c.append(re)
  fi=[]
  inp=[]
  for idx,fi in enumerate(c):
   inp.extend(['-i',fi])
   fi.append(f'[{idx}:a]')
  fc=''.join(fi)+f'concat=n={len(c)}:v=0:a=1[outa]'
  subprocess.run(['ffmpeg','-y',*inp,'-filter_complex',fc,'-map','[outa]','-c:a','libvorbis','-fflags','+genpts',o],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
 finally:
  if os.path.exists(t):
   for f in os.listdir(t):os.remove(os.path.join(t,f))
   os.rmdir(t)
def getFileName(p):
 m=[]
 for f in os.listdir(p):
  if f.lower().endswith('.mc'):m.append(f)
 def e(f):
  b=os.path.splitext(f)[0]
  ma=re.search(r'(\d+)',b)
  return int(ma.group(1))if ma else 0
 m.sort(key=e)
 return m
def update_process_list(t):
 process_list.config(state=tk.NORMAL)
 process_list.insert(tk.END,t)
 process_list.see(tk.END)
 process_list.config(state=tk.DISABLED)
 process_list.update_idletasks()
def process_files():
 r=rest_var.get()
 b=beatrest_var.get()
 rg=re_gen_var.get()
 ti=title_entry.get()
 ar=artist_entry.get()
 di=directory_var.get()
 to=titleorg_entry.get()
 ao=artistorg_entry.get()
 ve=version_entry.get()
 ed=editor_entry.get()
 sc=scrollspeed_var.get()
 co=column_var.get()
 if not ti or not ar:messagebox.showerror("错误","歌曲标题和艺术家不能为空");return
 try:r=int(r);b=int(b)
 except ValueError:messagebox.showerror("错误","间隔时间和间隔小节必须是数字");return
 process_list.config(state=tk.NORMAL)
 process_list.delete(1.0,tk.END)
 process_list.insert(tk.END,"正在处理文件...\n")
 process_list.insert(tk.END,"="*50+"\n")
 process_list.config(state=tk.DISABLED)
 process_btn.config(state=tk.DISABLED)
 threading.Thread(target=process_files_thread,args=(r,b,rg,ti,ar,di,to,ao,ve,ed,sc,co)).start()
def transform_list(i):
 return[s[0]if s else{}for s in i]
def process_files_thread(r,b,rg,ti,ar,di,to,ao,ve,ed,sc,co):
 try:
  os.chdir(di)
  m=getFileName(".")
  au=[]
  pa=[]
  mn=[]
  b=0
  b1=0
  nt=[]
  ef=[]
  bl=[]
  for mc in m:
   with open(mc,'r',encoding='utf-8')as f:da=json.load(f)
   fn=os.path.basename(mc)
   t=da["meta"]["song"].get("titleorg","无标题")
   a=da["meta"]["song"].get("artistorg","未知艺术家")
   update_process_list(f"处理文件: {fn}\n")
   update_process_list(f"  标题: {t}\n")
   update_process_list(f"  艺术家: {a}\n")
   update_process_list("-"*50+"\n")
   au.append(da["note"][-1]["sound"])
   lb=da["note"][-2]["beat"][0]if len(da["note"])>=2 else 0
   for i in da["note"]:
    if"endbeat"in i:
     if lb<i["endbeat"][0]:lb=i["endbeat"][0]
   lb+=b
   pa.append({"bpm":da["time"][0]["bpm"],"offset":da["note"][-1]["offset"],"beats":lb})
   b1=b+lb
   bl.append(b)
   for i in da["time"]:
    if i["beat"][0]!=da["time"][0]["beat"][0]and"delay"not in i:i["delay"]=0.0
   nt.append([{"beat":[b,0,1],"bpm":da["time"][0]["bpm"],"delay":da["note"][-1]["offset"]if b==0 else r}]+[{"beat":[b+i["beat"][0]-da["time"][0]["beat"][0],0,1],"bpm":i["bpm"],"delay":i["delay"]}for i in da["time"]if i["beat"][0]!=da["time"][0]["beat"][0]])
   for n in da["note"][:-1]:
    if"endbeat"in n:mn.append({"beat":[n["beat"][0]+b,n["beat"][1],n["beat"][2]],"column":n["column"],"endbeat":[n["endbeat"][0]+b,n["endbeat"][1],n["endbeat"][2]]})
    else:mn.append({"beat":[n["beat"][0]+b,n["beat"][1],n["beat"][2]],"column":n["column"]})
   b=b1
  nt=transform_list(nt)
  for i in range(0,len(bl)):ef.append({"beat":[bl[i],0,1],"scroll":float(sc)/pa[i]["bpm"]})
  process_list.insert(tk.END,"正在生成混合音频...\n")
  process_list.update_idletasks()
  ao=f"output/mix_{ve}.ogg"
  if not os.path.exists("./output"):os.makedirs("./output")
  if rg:
   if os.path.exists(ao)and rg:os.remove(ao);update_process_list(f"删除旧音频文件: mix_{ve}.ogg\n")
   outputmixogg(au,pa,r,ao)
   update_process_list(f"已生成混合音频文件: mix_{ve}.ogg\n")
  else:update_process_list(f"使用现有音频文件: mix_{ve}.ogg\n")
  update_process_list("正在创建谱面文件...\n")
  process_list.update_idletasks()
  mj={"meta":{"id":0,"creator":ed,"version":ve,"mode":0,"song":{"title":ti,"artist":ar,"titleorg":to,"artistorg":ao,"file":f"mix_{ve}.ogg","bpm":max([i["bpm"]for i in pa])},"mode_ext":{"column":co,"bar_begin":0},"aimode":""},"effect":ef,"time":nt,"note":mn+[{"beat":[0,0,1],"type":1,"sound":f"mix_{ve}.ogg"}]}
  op=os.path.join(di,"output",f"mix_{ve}.mc")
  with open(op,'w',encoding='utf-8')as f1:json.dump(mj,f1,indent=2,ensure_ascii=False)
  process_list.insert(tk.END,"="*50+"\n")
  process_list.insert(tk.END,f"处理完成！输出文件: {op}\n")
  process_list.see(tk.END)
  process_btn.config(state=tk.NORMAL)
  messagebox.showinfo("完成",f"处理完成！输出文件在: {op}")
 except Exception as e:messagebox.showerror("遇到此错误弹窗请寻求开发者帮助：qq群451586654","错误"+str(e))
def select_directory():
 d=filedialog.askdirectory()
 if d:directory_var.set(d)
def check_ffmpeg():
 try:
  subprocess.run(['ffmpeg','-version'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,check=True)
  return True
 except:return False
try:
 from ctypes import windll
 windll.shcore.SetProcessDpiAwareness(1)
except:pass
root=tk.Tk()
root.configure(bg="#F2F3F9")
style=ttk.Style()
style.configure('TFrame',background='#F2F3F9')
style.configure('TLabelframe',background='#F2F3F9')
style.configure('TLabelframe.Label',background='#F2F3F9')
style.configure('TLabel',background='#F2F3F9')
style.configure('TButton',background="#F2F3F9")
style.configure('TEntry',background='#F2F3F9')
style.configure('TCheckbutton',background='#F2F3F9')
style=ttk.Style()
at=style.theme_names()
if"vista"in at:style.theme_use("vista")
elif"winnative"in at:style.theme_use("winnative")
elif"alt"in at:style.theme_use("alt")
else:style.theme_use("clam")
try:
 from ctypes import windll
 windll.shcore.SetProcessDpiAwareness(1)
except:pass
df=("Microsoft YaHei",10)
root.option_add("*Font",df)
root.title(f"MCZ合谱工具v{CURRENT_VERSION}@MCPsDEPE")
root.geometry("900x750")
try:root.iconbitmap(resource_path("asd.ico"))
except:pass
def download_ffmpeg(u,s):
 try:urllib.request.urlretrieve(u,s);return True
 except Exception as e:messagebox.showerror("下载错误",f"下载ffmpeg失败: {str(e)}");return False
if not check_ffmpeg():
 u="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
 t=tempfile.mkdtemp()
 z=os.path.join(t,"ffmpeg.zip")
 e=os.path.join(os.getcwd(),"ffmpeg.exe")
 if download_ffmpeg(u,z):
  try:
   import zipfile
   with zipfile.ZipFile(z,'r')as zr:
    for f in zr.namelist():
     if f.endswith("ffmpeg.exe"):
      with open(e,'wb')as fw:fw.write(zr.read(f))
      break
   os.environ["PATH"]=os.getcwd()+os.pathsep+os.environ["PATH"]
   if not check_ffmpeg():messagebox.showerror("错误","自动安装ffmpeg失败，请手动安装并添加到系统PATH");sys.exit(1)
   messagebox.showinfo("成功","ffmpeg已自动下载并安装到当前目录")
  except Exception as e:messagebox.showerror("错误",f"解压ffmpeg失败: {str(e)}");sys.exit(1)
  finally:
   if os.path.exists(z):os.remove(z)
   if os.path.exists(t):os.rmdir(t)
 else:sys.exit(1)
rest_var=tk.StringVar(value="5000")
beatrest_var=tk.StringVar(value="1")
scrollspeed_var=tk.StringVar(value="180")
re_gen_var=tk.BooleanVar(value=True)
directory_var=tk.StringVar(value=os.getcwd())
title_var=tk.StringVar()
artist_var=tk.StringVar()
titleorg_var=tk.StringVar(value="MCPsDEPE's MCZ tool")
artistorg_var=tk.StringVar(value="Various Artists")
version_var=tk.StringVar(value="4K - MCPsDEPE Lv.999")
editor_var=tk.StringVar(value="Yourname&MCPsDEPE(此处填写谱师与鸣谢信息)")
column_var=tk.StringVar(value="4")
frame=ttk.Frame(root,padding="10")
frame.pack(fill=tk.BOTH,expand=True,padx=10,pady=10)
frame.configure(style='TFrame')
dir_frame=ttk.LabelFrame(frame,text="工作目录",padding="10")
dir_frame.pack(fill=tk.X,pady=5)
ttk.Label(dir_frame,text="工作目录:").grid(row=0,column=0,sticky=tk.W,pady=5)
ttk.Entry(dir_frame,textvariable=directory_var,width=60).grid(row=0,column=1,padx=5)
ttk.Button(dir_frame,text="浏览",command=select_directory).grid(row=0,column=2)
param_frame=ttk.LabelFrame(frame,text="合成参数",padding="10")
param_frame.pack(fill=tk.X,pady=5)
ttk.Label(param_frame,text="间隔时间(ms):").grid(row=0,column=0,sticky=tk.W,pady=5)
ttk.Entry(param_frame,textvariable=rest_var,width=10).grid(row=0,column=1,sticky=tk.W,padx=5)
ttk.Label(param_frame,text="间隔小节:").grid(row=0,column=2,sticky=tk.W,pady=5,padx=(20,0))
ttk.Entry(param_frame,textvariable=beatrest_var,width=10).grid(row=0,column=3,sticky=tk.W,padx=5)
ttk.Label(param_frame,text="轨道数量:").grid(row=0,column=4,sticky=tk.W,pady=5,padx=(20,0))
ttk.Entry(param_frame,textvariable=column_var,width=10).grid(row=0,column=5,sticky=tk.W,padx=5)
ttk.Label(param_frame,text="BPM值:").grid(row=1,column=0,sticky=tk.W,pady=5)
ttk.Entry(param_frame,textvariable=scrollspeed_var,width=10).grid(row=1,column=1,sticky=tk.W,padx=5)
ttk.Checkbutton(param_frame,text="生成音频文件",variable=re_gen_var).grid(row=1,column=2,sticky=tk.W,padx=(20,0))
song_frame=ttk.LabelFrame(frame,text="歌曲信息",padding="10")
song_frame.pack(fill=tk.X,pady=5)
ttk.Label(song_frame,text="歌曲标题(英文):").grid(row=0,column=0,sticky=tk.W,pady=5)
title_entry=ttk.Entry(song_frame,textvariable=title_var,width=40)
title_entry.grid(row=0,column=1,sticky=tk.W,padx=5)
ttk.Label(song_frame,text="艺术家(英文):").grid(row=1,column=0,sticky=tk.W,pady=5)
artist_entry=ttk.Entry(song_frame,textvariable=artist_var,width=40)
artist_entry.grid(row=1,column=1,sticky=tk.W,padx=5)
ttk.Label(song_frame,text="歌曲标题(原文):").grid(row=2,column=0,sticky=tk.W,pady=5)
titleorg_entry=ttk.Entry(song_frame,textvariable=titleorg_var,width=40)
titleorg_entry.grid(row=2,column=1,sticky=tk.W,padx=5)
ttk.Label(song_frame,text="艺术家(原文):").grid(row=3,column=0,sticky=tk.W,pady=5)
artistorg_entry=ttk.Entry(song_frame,textvariable=artistorg_var,width=40)
artistorg_entry.grid(row=3,column=1,sticky=tk.W,padx=5)
ttk.Label(song_frame,text="难度版本(version):").grid(row=4,column=0,sticky=tk.W,pady=5)
version_entry=ttk.Entry(song_frame,textvariable=version_var,width=40)
version_entry.grid(row=4,column=1,sticky=tk.W,padx=5)
ttk.Label(song_frame,text="谱面作者(Editor):").grid(row=5,column=0,sticky=tk.W,pady=5)
editor_entry=ttk.Entry(song_frame,textvariable=editor_var,width=40)
editor_entry.grid(row=5,column=1,sticky=tk.W,padx=5)
btn_frame=ttk.Frame(song_frame)
btn_frame.grid(row=0,column=2,rowspan=2,padx=20,pady=5,sticky=tk.NSEW)
process_btn=ttk.Button(btn_frame,text="开始处理",command=process_files,width=25)
process_btn.pack(fill=tk.BOTH,expand=True,padx=5,pady=5)
list_frame=ttk.LabelFrame(frame,text="处理进度",padding="10")
list_frame.pack(fill=tk.BOTH,expand=True,pady=5)
process_list=scrolledtext.ScrolledText(list_frame,width=90,height=20)
process_list.pack(fill=tk.BOTH,expand=True,padx=5,pady=5)
process_list.insert(tk.END,"等待处理...\n")
process_list.config(state=tk.DISABLED)
check_updates_on_start()
root.mainloop()